package tech

import (
	"maps"
	"slices"
	"ssp/set"
)

func (s *Solve) BinaryUniversalGraveType1(g *Grid) (used bool, results []*Result) {
	triples := g.scanBiValueCells()
	if len(triples) != 1 {
		return
	}
	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != 1 || len(trueCands) != 1 || trueCands[0].Count() != 1 {
		return
	}

	cell := trueCandCells[0]
	value := toValue(trueCands[0].GetList()[0])
	//g.solveOneCell(cell.rowNum, cell.colNum, value)
	result := new(Result)
	result.setChosen(value, cell)
	result.setStrategyName("BUG Type 1")
	results = append(results, result)
	used = true
	return
}

func (g *Grid) searchTrueCands(triples []*Cell) (
	trueCandCells []*Cell, trueCands []set.Set) {
	change := true
	for change {
		change = false
		for _, triple := range triples {
			if slices.Contains(trueCandCells, triple) {
				continue
			}
			cands := g.fetchTrueCands(triple, trueCandCells, trueCands)
			if cands.Count() == triple.cands.Count()-2 {
				trueCandCells = append(trueCandCells, triple)
				trueCands = append(trueCands, cands)
				change = true
			}
		}
	}
	if len(trueCandCells) != len(triples) {
		return []*Cell{}, []set.Set{}
	}
	return trueCandCells, trueCands
}

func (g *Grid) scanBiValueCells() []*Cell {
	cells := []*Cell{}
	for _, row := range g.rows {
		for _, cell := range row.cells {
			if cell.solved() {
				continue
			}
			count := cell.cands.Count()
			if count == 2 {
				continue
			}
			if count >= 3 {
				cells = append(cells, cell)
				continue
			}
			return []*Cell{}
		}
	}
	return cells
}
func (g *Grid) fetchTrueCands(cell *Cell, trueCandCells []*Cell, trueCands []set.Set) (
	cands set.Set) {
	candsr := g.fetchTrueCandsInHouse(cell, trueCandCells, trueCands, g.rows[cell.rowNum])
	candsc := g.fetchTrueCandsInHouse(cell, trueCandCells, trueCands, g.cols[cell.colNum])
	candsb := g.fetchTrueCandsInHouse(cell, trueCandCells, trueCands, g.boxes[cell.boxNum])
	cands = candsr.Intersect(candsc).Intersect(candsb)
	return
}

func (g *Grid) fetchTrueCandsInHouse(theCell *Cell, trueCandCells []*Cell,
	trueCands []set.Set, house House) (cands set.Set) {
	candCounts := [9]int{}
	for _, cell := range house.cells {
		if cell.solved() {
			continue
		}
		for _, cand := range cell.cands.GetList() {
			skip := false
			for i, t := range trueCandCells {
				if cell == t && trueCands[i].Has(cand) {
					skip = true
					break
				}
			}
			if skip {
				continue
			}
			candCounts[cand]++
		}
	}
	for cand, count := range candCounts {
		if count == 2 || count == 0 {
			continue
		}
		if count > 2 && theCell.cands.Has(cand) {
			cands.Add(cand)
		}
	}
	return
}

func (s *Solve) BinaryUniversalGraveType2(g *Grid) (used bool, results []*Result) {
	triples := g.scanBiValueCells()
	if len(triples) == 0 {
		return
	}
	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) == 1 && len(trueCands) == 1 && trueCands[0].Count() == 1 {
		return
	}

	var rmCells []*Cell
	var rmCands []set.Set
	candCells := map[set.Set][]*Cell{}
	for i := range trueCands {
		candCells[trueCands[i]] = append(candCells[trueCands[i]], trueCandCells[i])
	}
	switch len(trueCandCells) {
	case 0:
		return
	case 1:
		rmCell := trueCandCells[0]
		rmCand := rmCell.cands.Diff(trueCands[0])
		rmCells = append(rmCells, rmCell)
		rmCands = append(rmCands, rmCand)
	case 2:
		if trueCands[0] == trueCands[1] {
			if trueCands[0].Count() == 1 {
				if ok, rs := g.canRemoveSeenCands(trueCands[0], trueCandCells); ok {
					rmCells = append(rmCells, rs...)
					rmCands = append(rmCands, trueCands[0])
				}
			}
		} else {
			if !isSameHouse(trueCandCells) {
				return
			}
			if trueCands[0].Count() == 1 &&
				trueCands[0].Intersect(trueCandCells[1].cands) == trueCands[0] {
				rmCells = append(rmCells, trueCandCells[1])
				rmCands = append(rmCands, trueCands[0])
			}
			if trueCands[1].Count() == 1 &&
				trueCands[1].Intersect(trueCandCells[0].cands) == trueCands[1] {
				rmCells = append(rmCells, trueCandCells[0])
				rmCands = append(rmCands, trueCands[1])
			}
		}
	default:
		switch len(candCells) {
		case 1:
			if trueCands[0].Count() != 1 {
				return
			}
			if ok, rs := g.canRemoveSeenCands(trueCands[0], trueCandCells); ok {
				rmCells = append(rmCells, rs...)
				rmCands = append(rmCands, trueCands[0])
			}
		case 2:
			keys := slices.Collect(maps.Keys(candCells))
			//fmt.Println(keys)
			var rmCell *Cell
			var rmCand, refCand set.Set
			if len(candCells[keys[0]]) == 1 {
				rmCell = candCells[keys[0]][0]
				rmCand = keys[1]
				refCand = keys[0]
			} else if len(candCells[keys[1]]) == 1 {
				rmCell = candCells[keys[1]][0]
				rmCand = keys[0]
				refCand = keys[1]
			} else {
				return
			}

			for _, c := range candCells[refCand] {
				if !isSameHouse([]*Cell{c, rmCell}) {
					return
				}
			}

			if rmCand.Intersect(rmCell.cands) != rmCand {
				return
			}
			rmCells = append(rmCells, rmCell)
			rmCands = append(rmCands, rmCand)
		default:
			return
		}
	}

	used = true
	result := new(Result)
	for i := range rmCells {
		result.addRemoved(rmCands[i], rmCells[i:i+1])
	}
	g1 := result.getHelpCandsGroup()
	for cand, cells := range candCells {
		g1.add(cand, cells)
	}
	//g1.add(set.NewSet(cand), triples)
	result.setStrategyName("BUG Type 2")
	results = append(results, result)
	return
}

func (s *Solve) BinaryUniversalGraveType3A(g *Grid) (used bool, results []*Result) {
	return g.binaryUniversalGrave3(g.verifyNaked, "BUG Type 3A")
}

func (s *Solve) BinaryUniversalGraveType3B(g *Grid) (used bool, results []*Result) {
	return g.binaryUniversalGrave3(g.verifyHidden, "BUG Type 3B")
}
func (g *Grid) binaryUniversalGrave3(verifyFunc func(set.Set, []*Cell, []*Cell) (
	bool, []*Cell, set.Set, []*Cell), sname string) (used bool, results []*Result) {
	triples := g.scanBiValueCells()
	if len(triples) < 2 {
		return
	}

	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}
	houses := []House{}
	if inHouse(trueCandCells, g.rows[trueCandCells[0].rowNum]) {
		houses = append(houses, g.rows[trueCandCells[0].rowNum])
	}
	if inHouse(trueCandCells, g.cols[trueCandCells[0].colNum]) {
		houses = append(houses, g.cols[trueCandCells[0].colNum])
	}
	if inHouse(trueCandCells, g.boxes[trueCandCells[0].boxNum]) {
		houses = append(houses, g.boxes[trueCandCells[0].boxNum])
	}

	for _, house := range houses {
		cands := allUion(trueCands)
		cells := []*Cell{}
		for _, cell := range house.cells {
			if cell.solved() || slices.Contains(trueCandCells, cell) {
				continue
			}
			cells = append(cells, cell)
		}
		if ok, subset, subsetCands, rmCells := verifyFunc(cands, trueCandCells, cells); ok {
			used = true
			result := new(Result)
			g1 := result.getHelpCandsGroup()
			for i := range trueCands {
				g1.add(trueCands[i], trueCandCells[i:i+1])
			}
			if sname == "BUG Type 3A" {
				result.addRemoved(subsetCands, rmCells)
			} else {
				result.addRemoved(set.NewFull().Diff(subsetCands), rmCells)
			}
			g2 := result.getHelpCandsGroup()
			g2.add(subsetCands, subset)
			g2.add(subsetCands, trueCandCells)

			result.setStrategyName(sname)
			results = append(results, result)
		}
	}
	return
}

func inHouse(cells []*Cell, house House) bool {
	switch house.kind {
	case 0:
		for _, cell := range cells {
			if cell.rowNum != house.cells[0].rowNum {
				return false
			}
		}
		return true
	case 1:
		for _, cell := range cells {
			if cell.colNum != house.cells[0].colNum {
				return false
			}
		}
		return true
	case 2:
		for _, cell := range cells {
			if cell.boxNum != house.cells[0].boxNum {
				return false
			}
		}
		return true

	}
	return false
}

func (s *Solve) BinaryUniversalGraveType4(g *Grid) (used bool, results []*Result) {
	triples := g.scanBiValueCells()
	if len(triples) != 2 {
		return
	}

	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}

	var house House
	if triples[0].rowNum == triples[1].rowNum {
		house = g.rows[triples[0].rowNum]
	} else if triples[0].colNum == triples[1].colNum {
		house = g.cols[triples[0].colNum]
	} else if triples[0].boxNum == triples[1].boxNum {
		house = g.boxes[triples[0].boxNum]
	} else {
		return
	}

	cands := set.NewSet()
	otherCands := set.NewSet()
	for _, cell := range house.cells {
		if cell.solved() {
			continue
		}
		if slices.Contains(trueCandCells, cell) {
			cands = cands.Union(cell.cands)
		} else {
			otherCands = otherCands.Union(cell.cands)
		}
	}
	trues := allUion(trueCands)
	cands = cands.Diff(trues).Diff(otherCands)
	if cands.Count() != 1 {
		return
	}

	rmCells := []*Cell{}
	rmCands := []set.Set{}
	for i, cell := range trueCandCells {
		rmCand := set.NewFull().Diff(trueCands[i]).Diff(cands)
		if cell.canRemoveCands(rmCand) {
			rmCells = append(rmCells, cell)
			rmCands = append(rmCands, rmCand)
		}
	}
	if len(rmCells) != 0 {
		result := new(Result)
		for i := range rmCands {
			result.addRemoved(rmCands[i], rmCells[i:i+1])
		}
		g1 := result.getHelpCandsGroup()
		for i := range trueCands {
			g1.add(trueCands[i], trueCandCells[i:i+1])
		}
		g2 := result.getHelpCandsGroup()
		g2.add(cands, trueCandCells)
		result.setStrategyName("BUG Type 4")
		results = append(results, result)
		used = true
	}

	return
}

func (s *Solve) BinaryUniversalGraveXZ(g *Grid) (used bool, results []*Result) {
	triples := g.scanBiValueCells()
	if len(triples) != 2 {
		return
	}

	if isSameHouse(triples) {
		return
	}

	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}

	if trueCands[0].Count() != 1 || trueCands[1].Count() != 1 {
		return
	}

	cands := trueCands[0].Union(trueCands[1])
	if cands.Count() != 2 {
		return
	}

	for i, cell := range trueCandCells {
		for _, seen := range g.seenCells(cell) {
			if seen.cands == cands {
				rmCand := trueCands[1-i]
				cells := []*Cell{trueCandCells[1-i], seen}
				if ok, rmCells := g.canRemoveSeenCands(rmCand, cells); ok {
					used = true
					result := new(Result)
					result.addRemoved(rmCand, rmCells)
					g1 := result.getHelpCandsGroup()
					for i := range trueCands {
						g1.add(trueCands[i], trueCandCells[i:i+1])
					}
					g2 := result.getHelpCandsGroup()
					g2.add(cands, []*Cell{seen})
					result.setStrategyName("BUG Type XZ")
					results = append(results, result)
					return
				}
			}
		}
	}
	return
}
